Document Title: [Boris - Understanding Mame Drivers 2.html (html file)]
Understanding MAME Drivers Volume 2: VIDHRDW by Daniel Boris Table of Contents 1.0 Introduction The purpose of this document is to help people understand how a MAME driver works and to help people who may want to write their own MAME drivers. A driver is a set of files that are part of the MAME source that define the operation of a specific arcade game. For this document I will be describing the video section of the driver for the game Zarzon. Before reading this document you may want to read over my Decoding Schematics document which describes the hardware of Zarzon, as well as the first volume of this document which describes the main driver structures for Zarzon. The driver I am going to describe is based on the MAME 0.33 source code for DOS. Some of the things described here may not apply to other versions of the MAME source. This volume will describe the functions in the VIDHRDW section of the driver. The file we will be looking at is /vidhrdw/rockola.c. 2.0 Colors Before we get into the driver it is a good idea to understand how games like Zarzon actually generate the graphics on the screen. In it's most basic form generating the screen is nothing more then determining what color each pixel on the screen should be. The graphics for Zarzon start out as a series of characters stored in graphics ROMS or RAMS. Each pixel in each character is represented by a number from 0 to 3. This number indirectly determines what color each pixel in the character will be. The important word here is "indirectly". If these numbers became the colors directly we would have a boring screen display. These color numbers pass through two more stages before they reach the screen. The first stage is the color map and the second is the palette. We'll look at the palette first. The colors you see on a video display are actually made up of different 'quantities' of 3 different colors, red, green, and blue. By varying the intensity of each of these 3 colors you can create any color you want. The job of the palette is to define the RGB levels for each color a game can display. The palette can be stored in either ROM (as it is in Zarzon) or in RAM. Each entry in the palette has a specific number of bits to represent the intensity of red, green and blue for each color. Zarzon has 3 bits for red, 3 bits for green and 2 bits for blue. So a palette entry for a bright red would be 0x07,0x00,0x00. If you wanted white you would have a palette entry of 0x07,0x07,0x03. In the circuitry of the game these binary values are converted to analog voltage levels that are used to control the CRT. Between the color values from the graphics ROMS/RAMS and the palette there is another layer called the color map (or color table). The color table is used to translate the color values into the palette index values. In a very simple color map (and a pretty pointless one), color value 0 would map to palette entry 0, color value 1 to palette entry 1, etc. This alone would serve no purpose so there is one more part to the puzzle. The video memory for Zarzon (like a lot of other games) stores the number of the character that is to be drawn at each location on the screen. The video memory also stores a color value for each location on the screen. This color value is combined with the color values from the characters and is used as a lookup into the color map. Here is an example of a simple color map: 0,1,2,3, 0,4,5,6, 0,7,8,9, 0,1,5,8 Each of the numbers in the table is a palette index number. When we draw a character on the screen we start with the color value from video memory and this tells us which line to use in the table. The pixel color values from the graphics ROMS are then used to select one of the 4 values on each line. So lets say that the first character on the screen has a color value of 2. The first pixel in the first character has a color value of 1. If you look this up in the table you would get a value of 7. This value would then be passed to the palette ROM to get the actual color that will displayed on the screen. If you notice, each line in the table starts with a 0. The purpose of this would be that anytime a pixel color value is a 0 it would map to palette color 0 which in most games is transparent, thus letting graphics overlap each other. 3.0 vh_convert_color_prom The first routine we want to look at is satansat_vh_convert_color_prom. This routine is called when the driver is first started and is used to create the palette for the game as well as the color table. The beginning of the routine looks like this: 1 void satansat_vh_convert_color_prom(unsigned char *palette, unsigned short *colortable,const unsigned char *color_prom) 2 { 3 int i; 4 #define TOTAL_COLORS(gfxn) (Machine->gfx[gfxn]->total_colors * Machine->gfx[gfxn]->color_granularity) 5 #define COLOR(gfxn,offs) (colortable[Machine->drv->gfxdecodeinfo[gfxn].color_codes_start + offs]) Line 1 is the start of the routine. All the arguments are pre defined in MAME so they must appear exactly like this. Lines 4 and 5 define macros that will be used later to generate the color table. TOTAL_COLORS(gfxn) will calculate the total number of color table entries needed for a specific graphics layout (gfxn). COLOR(gfxn,offs) is a shortcut for referring to a specific colortable entry for a graphics layout. The next section of code converts the raw color data that was loaded from the color prom file into the appropriate palette values. 1 for (i = 0;i < Machine->drv->total_colors;i++) 2 { 3 int bit0,bit1,bit2; 4 5 6 /* red component */ 7 bit0 = (*color_prom >> 0) & 0x01; 8 bit1 = (*color_prom >> 1) & 0x01; 9 bit2 = (*color_prom >> 2) & 0x01; 10 *(palette++) = 0x21 * bit0 + 0x47 * bit1 + 0x97 * bit2; 11 /* green component */ 12 bit0 = (*color_prom >> 3) & 0x01; 13 bit1 = (*color_prom >> 4) & 0x01; 14 bit2 = (*color_prom >> 5) & 0x01; 15 *(palette++) = 0x21 * bit0 + 0x47 * bit1 + 0x97 * bit2; 16 /* blue component */ 17 bit0 = 0; 18 bit1 = (*color_prom >> 6) & 0x01; 19 bit2 = (*color_prom >> 7) & 0x01; 20 *(palette++) = 0x21 * bit0 + 0x47 * bit1 + 0x97 * bit2; 21 22 color_prom++; 23 } Line 1 starts a loop that will count through each of the colors that the game uses. The total number of colors was specified in the Machine Driver structure. Lines 7-10 determine the color value for the red component of the color. The variable color_prom is a pointer to the color prom data in memory. In line 7 we get a byte from the color_prom and mask off the first bit of the red component. Line 8 gets the second bit and line 9 gets the third bit. The variable palette is a pointer to where the palette data should be put in memory. The three constants in line 10 are based on the resistors that are used in the real circuit to create the analog voltage level for this color. Line 10 uses the bit values to calculate the actual red level and stores this in the palette. Lines 12-15 do the same thing as lines 7-10 but for the green component of the color. Lines 17-20 do almost the same thing as lines 7-10, but since the blue component only has 2 bits instead of 3 we simple set bit0 = 0. Finally in line 22 we increment the color prom pointer to access the next byte. The next section of code builds the color tables for this game. The color mapping for this game is done with a couple logic circuits so the tables can be calculated mathematically. Some games will use a PROM for the color mapping in which case the color table can be loaded from the PROM file. 1 backcolor = 0; /* background color can be changed by the game */ 2 3 for (i = 0;i < TOTAL_COLORS(0);i++) 4 COLOR(0,i) = 4 * (i % 4) + (i / 4); 5 6 for (i = 0;i < TOTAL_COLORS(1);i++) 7 { 8 if (i % 4 == 0) COLOR(1,i) = backcolor + 0x10; 9 else COLOR(1,i) = 4 * (i % 4) + (i / 4) + 0x10; 10 } Line 3 and 4 generate the color table for the foreground graphics plane. The data that is generated for this is: 0,4,8,12 1,5,9,13 2,6,10,14 3,7,11,15 So if a character is color 0, it's four colors will be 0,4,8,12. If it's color 2 it's four colors will be 2,6,10,14, etc. Lines 6 - 9 generate the color table for the background graphics plane. The data that is generated for this is: 16,20,24,28 16,21,25,29 16,22,26,30 16,23,27,31 You will notice that the values start at 16, this is because the background uses the second half of the palette. You will also notice that the first number in each line is 16. In Zarzon character color 0 for the background graphics plane is the background color. The game can change this color so the driver will modify this number in the colortable whenever the game wants to change the background color. The variable backcolor (which is setup in line 1) is used to hold the current background color. 3.1 rockola_characterram_w This routine is called whenever a write is made to the character shape memory. This section of memory is used to store the graphics data for the characters that are draw in the foreground graphics plane. This gets called from the MemoryWriteAddress structure. 1 void rockola_characterram_w(int offset,int data) 2 { 3 if (rockola_characterram[offset] != data) 4 { 5 dirtycharacter[(offset / 8) & 0xff] = 1; 6 rockola_characterram[offset] = data; 7 } 8 } The purpose of this routine is to do dirty character handling. Since the data for these characters is stored in RAM and changes constantly the graphics data can not be decoded ahead of time like it can if it's stored in ROM. So while the foreground is being drawn we have to constantly decode the characters. Once a character has been decoded once, it is a waste of time to decode it again unless it has changed. The purpose of this routine is to keep track of when a character's data gets changed so it can be re-decoded. Line 1 is the declaration of the routine. The parameters are the standard parameters that are used by any routine called from the MemoryWriteAddress structure. In line 3, the variable rockola_characterram was declared in the MemoryWriteAddress structure so that it points to the character RAM section of memory. offset is the offset from the beginning of this section of memory to the actual memory address that was written to. This memory region begins at 0x1000, so if the cpu writes to location 0x1055 then offset will equal 0x55. This line compares the data byte that is already in memory with the one being written to see if it has changed. The array dirtycharacter in line 5 is used to keep track of whether a character is "dirty" or "clean". When we redraw the screen we only need to re-decode the dirty characters. Each character is 8 bytes long so we divide the offset by 8 to get the character number. We then set a flag for that character to indicate that it is now dirty. Line 6 writes the new data into memory. Since we have defined our own routine to handle writes to this memory region, MAME will not do the write for us so we have to do it manually. 3.2 satansat_b002_w This routine is another memory write handler. It handles writes to the memory location 0xB002. 1 void satansat_b002_w(int offset,int data) 2 { 3 /* bit 0 flips screen */ 4 if (flipscreen != (data & 0x01)) 5 { 6 flipscreen = data & 0x01; 7 memset(dirtybuffer,1,videoram_size); 8 } 9 10 /* bit 1 enables interrupts */ 11 /* it controls only IRQs, not NMIs. Here I am affecting both, which */ 12 /* is wrong. */ 13 interrupt_enable_w(0,data & 0x02); 14 15 /* other bits unused */ 16 } Line 1 is the standard declaration for a memory write routine. Line 4 checks to see if the flipscreen control bit has changed or not. flipscreen is used when the machine is in cocktail table mode to invert the display for the second player. Since the entire screen will be marked dirty we want to avoid doing this if the flipscreen bit didn't change. Line 6 stores the new state of the flip screen bit in the variable flipscreen. Line 7 sets all the tiles as being dirty so they will all be updated next time the screen is redrawn. The dirty tiles is a little different from the dirty character we described in the last section. Line 13 enables or disables the interrupts for this processor based on the state of bit 1 in data. 3.3 satansat_backcolor_w This routine is called whenever a write is done to location 0xB003. The purpose of this routine is to change the background color used to draw the background graphics plane. 1 void satansat_backcolor_w(int offset, int data) 2 { 3 /* bits 0-1 select background color. Other bits unused. */ 4 if (backcolor != (data & 3)) 5 { 6 int i; 7 8 backcolor = data & 3; 9 for (i = 0;i < 16;i += 4) 10 Machine->gfx[1]->colortable[i] = Machine->pens[backcolor + 0x10]; 11 memset(dirtybuffer,1,videoram_size); 12 } 13 } Line 4 checks whether the new the background color is actually being changed. No point in wasting time changing the color when it didn't really change. Line 8 masks off the lower 3 bits that are being written which are the backcolor. Line 9 and 10 steps through the colortable and sets the new background color for each of the 4 character color values. Line 11 marks all the tiles dirty so that they will all be updated next time the screen is refreshed. 3.4 satansat_vh_screenrefresh This routine is the real work horse for the video section of the driver. This routine is called once per frame to redraw the screen. The first section of this routine will redraw the background graphics plane. 1 void satansat_vh_screenrefresh(struct osd_bitmap *bitmap,int full_refresh) 2 { 3 int offs; 4 5 /* for every character in the Video RAM, check if it has been modified */ 6 /* since last time and update it accordingly. */ 7 for (offs = videoram_size - 1;offs >= 0;offs--) 8 { 9 if (dirtybuffer[offs]) 10 { 11 int sx,sy; 12 13 dirtybuffer[offs] = 0; 14 15 sx = offs % 32; 16 sy = offs / 32; 17 if (flipscreen) 18 { 19 sx = 31 - sx; 20 sy = 27 - sy; 21 } 22 23 drawgfx(tmpbitmap,Machine->gfx[1], 24 videoram[offs], 25 (colorram[offs] & 0x0C) >> 2, 26 flipscreen,flipscreen, 27 8*sx,8*sy, 28 &Machine->drv->visible_area,TRANSPARENCY_NONE,0); 29 } 30 } 31 32 /* copy the temporary bitmap to the screen */ 33 copybitmap(bitmap,tmpbitmap,0,0,0,0,&Machine->drv->visible_area,TRANSPARENCY_NONE,0); Line 1 is the standard declare for the vh_screenrefresh routine. Line 7 starts a loop that will step through each character in video memory. videoram_size was automatically setup by MAME based on the the MemoryWriteAddress structure. Line 9 is part of MAME's dirty rectangle handling. Dirty rectangle handling works like this; the first time we draw the screen we draw every character on the screen and hold this in a temporary buffer as well as drawing it to the screen. Next we use the dirtybuffer to mark every character as being clean. When the game writes to video memory the memory handler determines which character was changed and marks it as dirty in the dirtybuffer. Now the next time we redraw the screen we only have to redraw the characters that are dirty. Line 9 checks to see if the current tile is dirty or not. It only needs to be draw if it is dirty. Line 13 marks the current character as clean. Lines 15 and 16 calculate the x and y coordinate of the current tile on the screen. Lines 17 to 21 handle the cocktail flip for the game. This will cause the screen to be flipped both horizontally and vertically. Lines 23 to 28 are the call to MAME's drawgfx routine. This routine is the main graphics drawing routine that most of the games in MAME use to draw character graphics to the screen. Here is what each parameter does: tmpbitmap - This is the bitmap you want the graphic drawn to. Machine->gfx[1] - This is the gfxlayout entry that you want to use to draw this graphic. This was defined in the GfxLayout structure in /drivers/rockola.c videoram[offs] - This is the number of the character in the character set you want to draw. (colorram[offs] & 0x0C) >> 2 - This is the color number you want to use to draw the character. This will be used as a lookup into the colortable to get the actual colors used to draw the character. flipscreen,flipscreen - These two parameter indicate if the graphic should be flipped in the X direction and Y direction respectivly. If they are 0 then the graphic will not be flipped otherwise it will. 8*sx, 8*sy - These parameters specify the X and Y coordinate in pixels where you want the graphic to be drawn. In this case sx and sy are the x and y tile coordinates. Since the tiles are 8 pixels by 8 pixels you have to multiply x and y by 8 to get the pixel coordinates. &Machine->drv->visible_area - This parameter specifies the clipping area for the bitmap we are drawing onto. Anything beyond this clipping area will not be drawn. In this case we simply use the visible area from the Machine driver structure. TRANSPARENCY_NONE,0 - These parameters specify the transparency for the graphic. In this case, since we are drawing the background, no transparency is needed. Line 33 calls a routine called copybitmap() which move the graphics we just drew to the screen bitmap (which will eventually get moved to the screen). The parameters for this routine are as follows: bitmap - The destination bitmap, in this case the screen bitmap tmpbitmap - The source bitmap, in this case the background we just drew. 0,0 - These two values indicate if the image should be flipped horizontally or vertically when it is copied. 0,0 - These two values indicate the x and y coordinates in the destination bitmap to which you want to copy the source bitmap. In this case the source and destination are the same size so we are copying to 0,0. &machine->drv->visible_area - this is a pointer to a rectangle structure that defines the clipping areas of the destination bitmap. TRASNPARENCY_NONE,0 - this defines the transparency that should be used in the copy. Since this is the background there is not transparency needed. The second part of screen_refresh routine will draw the foreground graphics plane. The code in this section is very similar to that in the last section so I will only describe the differences. 1 /* draw the frontmost playfield. They are characters, but draw them as sprites */ 2 for (offs = videoram_size - 1;offs >= 0;offs--) 3 { 4 int charcode; 5 int sx,sy; 6 7 8 charcode = rockola_videoram2[offs]; 9 10 /* decode modified characters */ 11 if (dirtycharacter[charcode] != 0) 12 { 13 decodechar(Machine->gfx[0],charcode,rockola_characterram, 14 Machine->drv->gfxdecodeinfo[0].gfxlayout); 15 dirtycharacter[charcode] = 0; 16 } 17 18 sx = offs % 32; 19 sy = offs / 32; 20 if (flipscreen) 21 { 22 sx = 31 - sx; 23 sy = 27 - sy; 24 } 25 26 drawgfx(bitmap,Machine->gfx[0], 27 charcode, 28 colorram[offs] & 0x03, 29 flipscreen,flipscreen, 30 8*sx,8*sy, 31 &Machine->drv->visible_area,TRANSPARENCY_PEN,0); 33 } The biggest difference in this routine is lines 10-16. When we drew the background the data came out of ROM so all the graphics where decoded when that driver was initialized. The foreground graphics data comes from RAM so we have to decode the characters on the fly. Most of the characters are going to be drawn to the screen many times before their shape is changed, so it would be a waste to decode the character every time we are going to use it. This is where dirtycharacter handling comes in. We initially start with every character being marked as dirty. The first time the character is used it is decoded then is marked as being clean so that it does not have to be decoded again. As you saw earlier the routine rockola_characterram_w, will mark characters dirty again when their data is changed Line 11 checks to see if the current character is dirty. If it isn't then we can skip the decode. Lines 13 and 14 call the decodechar routine which decodes the graphics. This is the same routine that the MAME core uses to decode the ROM based graphics when the driver is initialized. The parameters are as follows: Machine->gfx[0] - This is a pointer to a GfxElement structure for the graphics you are decoding. MAME creates these structures when the driver is initialized. There is one for each entry in the gfxdecodeinfo[] structure in the driver file. charcode - Which character in the set should be decoded. rockola_characterram - A pointer to the location of the source data to be decoded. Machine->drv->gfxdecodeinfo[0].gfxlayout - A pointer to the gfxlayout structue to use in doing the decode. Line 15 marks the current character as clean since we just decoded it. The other important difference in this section of the code is in line 31. The drawgfx routine has the transparency set to TRANSPARENCY_PEN,0. When the graphics is drawn, any pixel that is color 0 will not be drawn so that it will not overwrite the background. Essentially the color 0 is transparent so you can see the background though it.